﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using MSAS.FrontEnd.DL;

namespace MRS.FrontEnd.BO
{
    public class UrlBinaryCodec : ReputationBinaryCodecBase, IReputationBinaryCodec
    {
        public static readonly byte Conf100 = 100;
        public static readonly byte Conf50 = 50;
        public static readonly byte Conf0 = 0;

        /// <summary>
        /// Name of the constants for the metadata in a URL UnpackObject dictionaries
        /// </summary>
        public const string MetadataNameBloom = "Bloom";
        public const string MetadataNameHostInheritance = "HostInheritance";
        public const string MetadataNamePathInheritance = "PathInheritance";

        // The URL Implementation of the ReputationBinary in the database skips the creation of the Bloom and indicates it
        // in the Flag byte of the fixed portion of the ReputationBinary.
        public static readonly byte[] EmptyBloom = Convert.FromBase64String("CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
        public static readonly byte[] FullBloom = Convert.FromBase64String("CP//////////////////////////////////////////");
        private static readonly int BloomLength = 33;

        /// <summary>
        /// Possible flag values for the URL
        /// </summary>
        public enum UrlFlags : byte
        {
            Empty = 0x00,       // bits 0 and 1
            Valid = 0x01,
            Invalid = 0x02,
            Full = 0x03
        }

        /// <summary>
        /// Flags to unpack the reputation category information in a URL ReputationBinary
        /// </summary>
        [Flags]
        private enum CategoryFlags : uint
        {
            Inherit = 0x00000000,
            Confidence100 = 0x10000000,
            Confidence0 = 0x20000000,
            Confidence50 = (Confidence100 | Confidence0),
            ConfidenceMask = (Confidence100 | Confidence0),
            HostInheritanceMask = 0x40000000,
            PathInheritanceMask = 0x80000000,
            CategoryIdMask = 0x0FFFFFFF,
            CategoryFlagsMask = 0xF0000000
        };

        /// <summary>
        /// Store an instance of this object for the singleton pattern
        /// </summary>
        private static UrlBinaryCodec _instance;

        /// <summary>
        /// Return the instance for the singleton pattern
        /// </summary>
        public static IReputationBinaryCodec Instance
        {
            get {
            
                if (_instance == null)
                {
                    _instance = new UrlBinaryCodec();
                }
                
                return _instance; 
            }
        }

        /// <summary>
        /// Private constructor for singleton pattern
        /// </summary>
        private UrlBinaryCodec()
        {
            try
            {
                ReputationBinaryCodecBase.cache = DBCache.GetInstance();
            }
            catch (TypeInitializationException ex)
            {
                throw ex.InnerException;
            }
        }

        /// <summary>
        /// Overloaded private constructor who can inject a ICache
        /// </summary>
        /// <param name="cacheInstance"></param>
        private UrlBinaryCodec(ICache cacheInstance)
        {
            ReputationBinaryCodecBase.cache = cacheInstance;
            UrlBinaryCodec._instance = this;
        }
        
        #region IReputationBinaryCodec Members
      
        /// <summary>
        /// Unpack a URL packed ObjectBinary as returned by the PSDB
        /// </summary>
        /// <param name="bytes"></param>
        /// <returns></returns>
        public UnpackedObject UnpackObjectBinary(byte[] bytes)
        {
            UnpackedObject unpack = new UnpackedObject();
            DecodeFixedPortion(bytes, unpack);

            ValidateFlags(bytes, unpack.Flags);

            // get the reputations and decode them
            int reputationCount = bytes.Skip((int)FixedPosition.Cats).First();

            ValidateBinaryLength(bytes, reputationCount, unpack.Flags);

            int index = FixedPortionLength;
            for (int i = 0; i < reputationCount; i++)
            {
                byte[] cat = bytes.Skip(index).Take(4).Reverse().ToArray();
                Reputation reputation = ParseReputation(BitConverter.ToUInt32(cat, 0));
                // We will get a null back from ParseReputation if the Confidence bits
                // were set to Inherit.  This just means that the variant that this 
                // set of categories applies to will just inherit that category data from
                // the parent.
                if (reputation != null)
                {
                    unpack.Reputations.Add(reputation);
                }
                index += 4;
            }

            byte[] bloom = ParseBloom((UrlFlags) unpack.Flags, bytes, index);
            if (bloom != null)
            {
                unpack.Metadatas.Add(MetadataNameBloom, bloom);
            }

            return unpack;
        }

        /// <summary>
        /// Pack a URL into a object binary
        /// </summary>
        /// <param name="unpackObject"></param>
        /// <returns></returns>
        public byte[] PackObjectBinary(UnpackedObject unpackObject)
        {
            // Calculate the Flag of the URL by checking the bloom
            //
            byte flag = (byte) UrlFlags.Valid;      // default the flag to valid

            if (!unpackObject.Metadatas.ContainsKey(MetadataNameBloom)
                || unpackObject.Metadatas[MetadataNameBloom] == null)
            {
                flag = (byte)UrlFlags.Invalid;
            }
            else
            {
                // check the bloom
                if (EmptyBloom.SequenceEqual((byte[])unpackObject.Metadatas[MetadataNameBloom]))
                {
                    flag = (byte)UrlFlags.Empty;
                }
                else if (FullBloom.SequenceEqual((byte[])unpackObject.Metadatas[MetadataNameBloom]))
                {
                    flag = (byte)UrlFlags.Full;
                }
            }
            
            // Create the packObject
            //
            List<byte> packObject = new List<byte>();
            
            // use the base class to encode the fixed portion
            packObject.AddRange(EncodeFixedPortion(unpackObject, unpackObject.Reputations.Count));
            packObject[(int)FixedPosition.Flags] = flag; // set the flag after it was created empty

            foreach (Reputation rep in unpackObject.Reputations)
            {
                packObject.AddRange(PackReputation(rep));
            }
            if ((UrlFlags) flag == UrlFlags.Valid)
            {
                packObject.AddRange((byte[])unpackObject.Metadatas[MetadataNameBloom]);
            }

            return packObject.ToArray();
        }

        #endregion

        #region Decoder functions

        /// <summary>
        /// Parse a packed reputation for Url
        /// </summary>
        /// <param name="bytes"></param>
        /// <returns></returns>
        private static Reputation ParseReputation(uint categoryPack)
        {
            Reputation reputation = new Reputation();

            byte confidence = 0;
            uint flags;

            // set the category id, the first 28 bits are the Id and the most significant 4 bits 
            // are for the flags.
            reputation.Category = (int)(categoryPack & (uint)CategoryFlags.CategoryIdMask);

            // Confidence
            flags = categoryPack & (uint)CategoryFlags.CategoryFlagsMask;

            uint temp = flags & (uint)CategoryFlags.ConfidenceMask;

            // Unpack the Confidence
            if (temp == (uint)CategoryFlags.Inherit)
            {
                // If the confidence is Inherit for some reason than this is really 
                // a no-op because no category is needed for this.  Therefore we 
                // should just return null as a marker and not put this one into the
                // list returned to the client.
                return null;
            }
            else if (temp == (uint)CategoryFlags.Confidence0)
            {
                confidence = UrlBinaryCodec.Conf0;
            }
            else if (temp == (uint)CategoryFlags.Confidence50)
            {
                confidence = UrlBinaryCodec.Conf50;
            }
            else if (temp == (uint)CategoryFlags.Confidence100)
            {
                confidence = UrlBinaryCodec.Conf100;
            }
            reputation.Confidence = confidence;

            // host inheritance as a boolean member
            reputation.Metadatas.Add(MetadataNameHostInheritance, (flags & (uint)CategoryFlags.HostInheritanceMask) != 0);
            
            // path inheritance
            reputation.Metadatas.Add(MetadataNamePathInheritance, (flags & (uint)CategoryFlags.PathInheritanceMask) != 0);
            
            return reputation;
        }

        /// <summary>
        /// Parse the bloom information from the byte pack 
        /// </summary>
        /// <param name="bytes">bytes array containing the information</param>
        /// <param name="index">index pass the reputations</param>
        private static byte[] ParseBloom(UrlFlags urlFlag, byte[] bytes, int beginBloomIndex)
        {
            if (bytes == null || bytes.Length < beginBloomIndex - 1)
            {
                throw new ArgumentException("bytes");
            }

            switch (urlFlag)
            {
                case UrlFlags.Invalid:
                    return null;

                case UrlFlags.Empty:
                    return EmptyBloom;

                case UrlFlags.Full :
                    return FullBloom;

                default :
                    if (bytes.Length < beginBloomIndex + BloomLength - 1)
                    {
                        throw new ArgumentException(string.Format("Not enough bytes in the array to extract a bloom.  Binary Reputation : 0x{0}",
                            BitConverter.ToString(bytes).Replace("-", "")));
                    }
                    else if (bytes[beginBloomIndex] > 0x20)
                    {
                        throw new ArgumentException(string.Format("Control byte was out of the range of 0x0-0x20. Binary Reputation : 0x{0}",
                            BitConverter.ToString(bytes).Replace("-", "")));
                    }
                    return bytes.Skip(beginBloomIndex).Take(BloomLength).ToArray();
            }
        }

        #endregion

        #region Encoder functions

        /// <summary>
        /// Pack a single reputation into a uint according to the PSDB contract
        /// </summary>
        /// <param name="rep"></param>
        /// <returns></returns>
        private IEnumerable<byte> PackReputation(Reputation rep)
        {
            uint categoryId = (uint) rep.Category;
            if (rep.Confidence.Equals(Conf0))
            {
                categoryId |= (uint)CategoryFlags.Confidence0;
            }
            else if (rep.Confidence.Equals(Conf100))
            {
                categoryId |= (uint)CategoryFlags.Confidence100;
            }
            else if (rep.Confidence.Equals(Conf50))
            {
                categoryId |= (uint)CategoryFlags.Confidence50;
            }

            if ((bool)rep.Metadatas["PathInheritance"])
            {
                categoryId |= (uint)CategoryFlags.PathInheritanceMask;
            }
            if ((bool)rep.Metadatas["HostInheritance"])
            {
                categoryId |= (uint)CategoryFlags.HostInheritanceMask;
            }
            return BitConverter.GetBytes(categoryId).Reverse();
        }

        #endregion

        #region Validation Methods

        /// <summary>
        /// Validate that the Flags field of the the UrlFlags field does not
        /// have any unneccessary bits set.
        /// </summary>
        /// <param name="bytes">The binary reputation as a byte[] that is being validated.</param>
        /// <param name="urlFlag">The flags field that tells what type of bloom is used for this binary data</param>
        private static void ValidateFlags(byte[] bytes, byte urlFlag)
        {
            if ((urlFlag & (~(byte)UrlFlags.Full)) != 0)
            {
                throw new ArgumentOutOfRangeException(
                    string.Format("One of the first six bits of the UrlFlags is non-zero.  Binary Reputation : 0x{0}",
                    BitConverter.ToString(bytes).Replace("-", "")));
            }
        }

        /// <summary>
        /// Validate that we do not have a UrlBinary that is longer or shorter
        /// than what is stated in the different flags and count fields.
        /// </summary>
        /// <param name="binaryReputation">The binary reputation as a byte[] that is being validated.</param>
        /// <param name="reputationCount">The number of reputations in the binary data</param>
        /// <param name="flags">The flags field that tells what type of bloom is used for this binary data</param>
        private static void ValidateBinaryLength(byte[] binaryReputation, int reputationCount, byte flags)
        {
            if ((flags == (byte)UrlFlags.Invalid) && reputationCount != 0)
            {
                // This case is used because the Invalid flag is only used today for marking the entry
                // as a Placeholder in the cache and should never have reputations associated with it.
                // The only way this can happen is if the data from the database is corrupt in some form.
                // The validation is called when getting data from the database and when taking it from the 
                // cache.
                throw new ArgumentOutOfRangeException(string.Format("UrlFlags.Invalid && categoryCount != 0.  Binary Reputation : 0x{0}",
                    BitConverter.ToString(binaryReputation).Replace("-", "")));
            }

            int expectedBloomLength = flags == (byte)UrlFlags.Valid ? BloomLength : 0;

            // The 4 here represents the length that each category data field can be.  It
            // is the length in bytes of a UInt32
            int expectedVariableDataLength = (reputationCount * 4) + expectedBloomLength;

            if ((binaryReputation.Length - FixedPortionLength) != expectedVariableDataLength)
            {
                throw new ArgumentOutOfRangeException(string.Format("Reputation Binary length not correct.  Binary Reputation : 0x{0} ",
                    BitConverter.ToString(binaryReputation).Replace("-", "")));
            }
        } 

        #endregion
    }
}
